from socket import socket
from datetime import datetime
from threading import Thread
from tkinter import ttk
from contextlib import contextmanager
import threading
import json
import os
import bidict
import time
import tkinter as tk
from tkinter import messagebox
from tkinter import scrolledtext
from tkinter import filedialog
import traceback
from utils import process_logger, COMCODE, DateEncoder


class Client(tk.Frame):
    def __init__(self, root):
        tk.Frame.__init__(self, root)
        self.root = root
        self.usr_id = 0
        self.create_frame()
        self.pack_frame()

    @contextmanager
    def dialog_manager(self):
        self.tx_dialog.config(state="normal")
        yield
        self.tx_dialog.config(state="disabled")

    def create_frame(self):
        self.topper = tk.Frame(self, borderwidth=2)
        self.topper_1 = tk.Frame(self.topper, borderwidth=0)
        self.topper_2 = tk.Frame(self.topper, borderwidth=0)
        self.body = tk.Frame(self)
        self.tailer = tk.Frame(self)

        self.lb_ip = tk.Label(self.topper_1, text="IP:")
        self.lb_port = tk.Label(self.topper_1, text="PORT:")
        self.lb_usr = tk.Label(self.topper_2, text="User:")
        self.lb_psd = tk.Label(self.topper_2, text="Password:")
        self.var_con = tk.StringVar(value="IDLE")
        self.var_sign = tk.StringVar(value="Unsign")
        self.var_ip = tk.StringVar(value="localhost")
        self.var_port = tk.StringVar(value="6789")
        self.var_usr = tk.StringVar(value="usr1")
        self.var_psd = tk.StringVar(value="usr1")
        self.lb_con = tk.Label(
            self.topper_1,
            textvariable=self.var_con,
            bg="yellow",
            width=20,
        )
        self.lb_sign = tk.Label(
            self.topper_2,
            textvariable=self.var_sign,
            bg="yellow",
            width=20,
        )

        self.et_ip = tk.Entry(
            self.topper_1, width=20, textvariable=self.var_ip
        )
        self.et_port = tk.Entry(
            self.topper_1, width=20, textvariable=self.var_port
        )
        self.bt_con = tk.Button(
            self.topper_1, text="Connect", command=self.con_start
        )
        self.bt_break = tk.Button(
            self.topper_1, text="Break", command=self.con_stop
        )

        self.et_usr = tk.Entry(
            self.topper_2, width=18, textvariable=self.var_usr
        )
        self.et_psd = tk.Entry(
            self.topper_2, width=17, textvariable=self.var_psd
        )
        self.bt_log = tk.Button(
            self.topper_2, text="Log in", command=self.login
        )
        self.bt_reg = tk.Button(
            self.topper_2, text="Register", command=self.register
        )

        self.tx_dialog = scrolledtext.ScrolledText(
            self.body, width=80, height=10
        )
        self.tx_input = tk.Text(self.body, width=80, height=5)
        self.tx_dialog.tag_config("server", foreground="red")
        self.tx_dialog.tag_config("client", foreground="blue")
        self.nt_usr = ttk.Notebook(self.body)
        self.tb_frd = tk.Frame(self.nt_usr)
        self.tb_usr = tk.Frame(self.nt_usr)
        self.tb_frd_ls = tk.Listbox(self.tb_frd)
        self.tb_usr_ls = tk.Listbox(self.tb_usr)
        self.bt_usr_add = tk.Button(
            self.tb_usr, text="Add Friend", command=self.addFriend
        )
        self.bt_frd_del = tk.Button(
            self.tb_frd, text="Del Friend", command=self.delFriend
        )

        self.nt_usr.pack(side="left")
        self.bt_send = tk.Button(
            self.tailer,
            width=10,
            heigh=2,
            text="Send",
            command=self.sendInputMsg,
        )
        self.bt_file = tk.Button(
            self.tailer,
            width=10,
            heigh=2,
            text="File",
            command=self.sendFile,
        )

    def pack_frame(self):

        self.lb_con.pack(side="left", padx=10)
        self.lb_ip.pack(side="left")
        self.et_ip.pack(side="left", padx=10)
        self.lb_port.pack(side="left")
        self.et_port.pack(side="left", padx=10)
        self.bt_con.pack(side="left")
        self.bt_break.pack(side="left")
        self.topper_1.pack(side="top")

        self.lb_sign.pack(side="left", padx=10)
        self.lb_usr.pack(side="left")
        self.et_usr.pack(side="left", padx=10)
        self.lb_psd.pack(side="left")
        self.et_psd.pack(side="left", padx=10)
        self.bt_log.pack(side="left")
        self.bt_reg.pack(side="left")
        self.topper_2.pack(side="top")
        self.topper.pack(side="top", pady=10, padx=10)

        self.nt_usr.add(self.tb_frd, text="Friend", padding=3)
        self.nt_usr.add(self.tb_usr, text="Other", padding=3)
        self.tb_frd_ls.pack()
        self.tb_usr_ls.pack()
        self.bt_usr_add.pack(anchor="center")
        self.bt_frd_del.pack(anchor="center")

        self.tx_dialog.pack()
        self.tx_input.pack()
        self.body.pack()

        self.bt_send.pack(side="left")
        self.bt_file.pack(side="left")
        self.tailer.pack()
        self.pack()

    @process_logger
    def login(self):
        def tmp():
            self.client = socket()
            try:
                self.client.connect(
                    (self.var_ip.get(), int(self.var_port.get()))
                )
                self.var_con.set("Server Connected")
                Thread(target=self.recvMsg, daemon=True).start()

            except:
                self.var_con.set("Connection Failed")
                self.client.close()
                self.con_stop()
            self.sendMsg(
                COMCODE["login"],
                self.usr_id,
                {
                    "usr": self.var_usr.get(),
                    "psd": self.var_psd.get(),
                },
            )

        if hasattr(self, "client") and self.client.fileno() != -1:
            self.sendMsg(
                COMCODE["login"],
                self.usr_id,
                {
                    "usr": self.var_usr.get(),
                    "psd": self.var_psd.get(),
                },
            )
        else:
            Thread(target=tmp, daemon=True).start()

    @process_logger
    def register(self):
        self.con_start()
        self.sendMsg(
            COMCODE["register"],
            self.usr_id,
            {"usr": self.var_usr.get(), "psd": self.var_psd.get()},
        )

    @process_logger
    def con_start(self):
        if hasattr(self, "client") and self.client.fileno() != -1:
            print("ALREADY CONNECTING")
            return
        with self.dialog_manager():
            self.tx_dialog.delete(0.0, tk.END)
        self.var_con.set("Connecting Server...")
        Thread(target=self.connect, daemon=True).start()

    @process_logger
    def con_stop(self):
        if self.var_con.get() != "Server Closed":
            self.var_con.set("IDLE")
        if hasattr(self, "client"):
            if self.client.fileno() != -1:
                try:
                    self.sendMsg(-1, self.usr_id, "EXIT")
                except:
                    pass
            with self.dialog_manager():
                self.tx_dialog.delete(0.0, tk.END)
            self.client.close()

    @process_logger
    def connect(self):
        self.client = socket()
        try:
            self.client.connect(
                (self.var_ip.get(), int(self.var_port.get()))
            )
            self.var_con.set("Server Connected")
            Thread(target=self.recvMsg, daemon=True).start()
            self.loadMsg()
        except:
            traceback.print_exc()
            self.var_con.set("Connection Failed")
            self.client.close()
            self.con_stop()

    @process_logger
    def showMsg(self, msg_item):
        if msg_item["type"] not in [
            COMCODE["server_text"],
            COMCODE["server_file"],
            COMCODE["customer_text"],
            COMCODE["customer_file"],
        ]:
            # print(msg_item)

            return
        with self.dialog_manager():
            sender = (
                "server"
                if msg_item["type"] == 0 or msg_item["type"] == 2
                else "client"
            )
            timemsg = (
                sender
                + ":"
                + msg_item["time"].strftime("%Y-%m-%d %H:%M:%S")
                + "\n"
            )
            self.tx_dialog.insert(tk.END, timemsg, sender)
            self.tx_dialog.insert(tk.END, str(msg_item["msg"]) + "\n")
        self.tx_dialog.see(tk.END)

    @process_logger
    def loadMsg(self):
        with open("local_customer", "r") as f:
            for line in f.readlines():
                if line == "":
                    return
                msg = json.loads(line[:-1])
                self.sendMsg(
                    msg["type"],
                    msg["user_id"],
                    msg["msg"],
                    m_time=datetime.strptime(
                        msg["time"], "%Y-%m-%d %H:%M:%S"
                    ),
                )
        with open("local_customer", "w") as f:
            f.write("")

    @process_logger
    def sendInputMsg(self):
        msg = self.tx_input.get("1.0", "end-1c")
        if msg != "":
            # 显示发送时间和发送消息
            self.sendMsg(1, self.usr_id, msg)
            self.tx_input.delete(0.0, tk.END)  # 删除输入框的内容
        else:
            messagebox.showinfo("警告", "不能发送空白信息！")

    @process_logger
    def sendFile(self):
        selectFiles = filedialog.askopenfilenames(
            title="可选择1个或多个文件"
        )  # askopenfilename 1次上传1个；askopenfilenames1次上传多个
        for selectFile in selectFiles:
            file_size = os.path.getsize(selectFile)
            self.sendMsg(
                3,
                self.usr_id,
                [file_size, os.path.basename(selectFile)],
            )
            with open(selectFile, "rb") as f:
                self.client.sendall(f.read())

    @process_logger
    def recvMsg(self):
        while True:
            try:
                msg_item = self.client.recv(500).decode() + "\n"
                print(msg_item)
                msg_item = json.loads(msg_item)
                msg_item["time"] = datetime.strptime(
                    msg_item["time"], "%Y-%m-%d %H:%M:%S"
                )
                if msg_item["type"] == -1:
                    print("Server Closed")
                    self.var_con.set("Server Closed")
                    self.client.close()
                    raise Exception("Server Closed")
                elif msg_item["type"] == 2:
                    self.recvFile(msg_item)
                elif msg_item["type"] == COMCODE["success"]:
                    if msg_item["msg"] == "login":
                        self.usr_id = msg_item["user_id"]
                        self.var_sign.set("Logged")
                    elif msg_item["msg"] == "register":
                        self.var_sign.set("Registered")

                elif msg_item["type"] == COMCODE["fail"]:
                    self.var_sign.set(msg_item["msg"])

                elif msg_item["type"] == COMCODE["user_list"]:
                    self.setUserList(
                        msg_item["friend_info"],
                        msg_item["other_info"],
                    )

                self.showMsg(msg_item)
            except:
                traceback.print_exc()
                break

        self.con_stop()

    @process_logger
    def recvFile(self, item):
        file_size, file_name = (
            item["msg"][0],
            item["msg"][1],
        )
        with open(file_name, "wb") as f:
            written_length = 0
            while written_length < file_size:
                if file_size - written_length >= 1024:
                    recv_chunk = self.client.recv(1024)
                else:
                    recv_chunk = self.client.recv(
                        file_size - written_length
                    )
                f.write(recv_chunk)
                written_length += len(recv_chunk)

    @process_logger
    def setUserList(self, friend_info, other_info):
        self.friend_info, self.other_info = (
            bidict.bidict(friend_info),
            bidict.bidict(other_info),
        )
        print(self.friend_info)

        self.tb_frd_ls.delete(0, self.tb_frd_ls.size() - 1)
        self.tb_frd_ls.insert("end", *list(friend_info.values()))
        self.tb_usr_ls.delete(0, self.tb_usr_ls.size() - 1)
        self.tb_usr_ls.insert("end", *list(other_info.values()))

    @process_logger
    def addFriend(self):
        name = self.tb_usr_ls.get(self.tb_usr_ls.curselection())
        user_id = self.other_info.inverse[name]
        self.sendMsg(COMCODE["add_friend"], self.usr_id, user_id)

    @process_logger
    def delFriend(self):

        name = self.tb_frd_ls.get(self.tb_frd_ls.curselection())
        user_id = self.friend_info.inverse[name]
        self.sendMsg(COMCODE["del_friend"], self.usr_id, user_id)

    @process_logger
    def sendMsg(
        self, type, user_id, msg, is_show=True, m_time=datetime.now()
    ):
        msg_item = {"type": type, "user_id": user_id, "time": m_time}
        if isinstance(msg, dict):
            msg_item.update(msg)
        else:
            msg_item["msg"] = msg
        # print(msg)
        if is_show:
            self.showMsg(msg_item)

        if hasattr(self, "client") and self.client.fileno() != -1:
            msg_item = bytes(
                json.dumps(msg_item, cls=DateEncoder), encoding="utf8"
            )
            self.client.send(msg_item)
        else:
            with open("local_customer", "a") as f:
                f.write(json.dumps(msg_item, cls=DateEncoder) + "\n")
        return msg_item

    @process_logger
    def close_server(self):
        # t2._stop()
        self.con_stop()
        self.root.destroy()


def main():

    window = tk.Tk()
    window.title("Python chatbox customer")

    client = Client(window)
    window.protocol("WM_DELETE_WINDOW", client.close_server)

    client.mainloop()


if __name__ == "__main__":
    main()